/**
 * Copyright 2010 VTT Finnish Technical Research Centre
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package org.osami.syncagent.server;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.Socket;
import java.net.SocketException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import org.osami.commons.deviceidentifier.IDeviceIdentifier;
import org.osami.syncagent.SyncAgentService;
import org.osami.syncagent.impl.Activator;
import org.osgi.framework.ServiceReference;
import org.osgi.service.event.Event;

/**
 * Singular server thread which listens clients and invokes listener thread one per client.
 * Server invokes a new thread when ever client connects to it.  
 * @author elekko
 *
 */
public class SyncAgentServer{
	private Vector<URI> clientList;
	public SyncAgentService localSyncAgent;
	public int maxConnections = 55;
	private Thread bct;
	private SyncAgentBroadCastClient bcServer;
	private SyncAgentServerThread serverThread;
	//Vector SyncAgentServerListenerTCPIP	
	
	//JRO puukko; TODO find a better place for the message topics
	public static String REGISTRATION_EVENT = "org/osami/commons/Register";
	public static String DEREGISTRATION_EVENT = "org/osami/commons/Deregister";
	
	//JRO
	private Vector <String> absentClients = new Vector<String>();		

	public SyncAgentServer(SyncAgentService activator) {
		localSyncAgent = activator;
		clientList = new Vector();
		serverThread = new SyncAgentServerThread(this);
		Thread t = new Thread(serverThread);
        t.start();
        startBroadcastListener();
        IDeviceIdentifier di;
        String deviceId = "";        
       
        //Determine who I am
        ServiceReference ref = Activator.getBundleContext().getServiceReference(IDeviceIdentifier.class.getName());		
        if (null == ref) {
			System.out.println("DeviceIdentifier service not found - assuming dreambox");
			deviceId = "dreambox";
		} else {
			di = (IDeviceIdentifier)Activator.getBundleContext().getService(ref);
			deviceId = di.GetDeviceId();
		}
		// end JRO puukko
        
        // send registration message
        
        try {
			sendBroadCastMessage("register;osami://"+getLocalAddress().getHostAddress() + ":" + Activator.SERVER_PORT
					+";"+deviceId);
		} catch (UnknownHostException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	public void startBroadcastListener() {
		/**
		 * Send a broadcast message to network that new server has been created
		 */
        //InetAddress group = InetAddress.getByName(InetAddress.getLocalHost());
        InetAddress bcAddr = null;
        //DatagramSocket socket = null;
		try {
			bcAddr = getBroadCastAddress();
			//socket = new DatagramSocket(BC_PORT, bcAddr);
			//socket.setBroadcast(true);
		} catch (UnknownHostException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		/**
		 *  Start broadcast listener thread
		 */
		bcServer =  new SyncAgentBroadCastClient(bcAddr.getHostAddress(), this);
		bct = new Thread(bcServer);
		bct.start();
		System.out.println("SyncAgentActivator: Broadcast thread started");		
	}
	
	public void sendBroadCastMessage(String message)
	{

		/**
		 * Send broadcast message
		 */
        DatagramPacket packet;
        byte[] buf = new byte[256];
		try {			
			System.out.println("SyncAgentActivator: Broadcast message: " + message);
			buf = message.getBytes();
			packet = new DatagramPacket(buf, buf.length, getBroadCastAddress(), Activator.BC_PORT);
        	System.out.println("SyncAgentActivator: Sending the broadcast packet");
			bcServer.sendPacket(packet);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	/**
	 * Return computers real IP address. This is crap shoot since the returned address is 
	 * the last read non-loopback address. Better approad would be to return an array of addresses
	 * which are real, then client would work also on gateway device with multiple interfaces. 
	 * @return address of last real network interface
	 * @throws UnknownHostException
	 */
	private InetAddress getLocalAddress() throws UnknownHostException {
		InetAddress localAddr = InetAddress.getLocalHost();
		//String localAddr = locallAddr.getHostAddress();
		
		if (localAddr.isLoopbackAddress())
		{
			System.out.println("Error local address is loopback!!");
			NetworkInterface iface = null;
			try {
				for(Enumeration<NetworkInterface> ifaces = NetworkInterface.getNetworkInterfaces();ifaces.hasMoreElements();){
					iface = (NetworkInterface)ifaces.nextElement();
					System.out.println("Interface:"+ iface.getDisplayName());
					InetAddress ia = null;
					for(Enumeration ips = iface.getInetAddresses();ips.hasMoreElements();){
						ia = (InetAddress)ips.nextElement();
						if (!ia.isLoopbackAddress())
						{
							System.out.println("Found non loopback addr: " + ia.toString());
							localAddr = ia;
							//localAddr = ia.getHostAddress();
						}
						System.out.println(ia.getCanonicalHostName()+" "+ ia.getHostAddress());
					}
				}
			} catch (SocketException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
			
		}
		return localAddr;
	}
	
	/**
	 * Get broadcast address of the subnet to which the computer is registered.
	 * @return
	 * @throws UnknownHostException 
	 */
//	private InetAddress getBroadCastAddress() throws UnknownHostException {
//		
//		try {
//			Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
//			while (interfaces.hasMoreElements()) {
//				NetworkInterface networkInterface = interfaces.nextElement();
//				if (networkInterface.isLoopback())
//					continue;    // Don't want to broadcast to the loopback interface
//				for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
//					InetAddress broadcast = interfaceAddress.getBroadcast();
//					if (broadcast == null)
//						continue;
//			    
//			    	System.out.println("Found broadcast address: " + broadcast.toString());
//			    	return broadcast;
//			    
//				}
//			}
//		} catch (Exception ex) {
//			System.out.println(ex.getMessage());
//		}
//		
//		return InetAddress.getByName("255.255.255.255"); // ei näin
//	}	

		private InetAddress getBroadCastAddress() throws UnknownHostException {
	        try {
	            for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) {
	                NetworkInterface intf = en.nextElement();
	                for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements();) {
	                    InetAddress inetAddress = enumIpAddr.nextElement();
	                    if (!inetAddress.isLoopbackAddress()) {
	                    	byte[] addrBytes = inetAddress.getAddress();
	                    	//generate a broadcast address
							  if (addrBytes[0] < (byte)128)
							  {
							  	byte [] mask = { (byte)255, 0, 0, 0 };
							  	for (int i=0; i < 4; i++) {
							          addrBytes[i] |= ((byte)0xFF) ^ mask[i];
							        }
							  }
							  /*
							   * B-class IPv4
							   */
							  else if (addrBytes[0] < (byte)192)
							  {
							  	byte [] mask = { (byte)255, (byte)255, 0, 0 };
							  	for (int i=0; i < 4; i++) {
							          addrBytes[i] |= ((byte)0xFF) ^ mask[i];
							        }
							  }
							  /*
							   * C-class IPv4
							   */
							  else 
							  {
							  	byte [] mask = { (byte)255, (byte)255, (byte)255, 0 };
							  	for (int i=0; i < 4; i++) {
							          addrBytes[i] |= ((byte)0xFF) ^ mask[i];
							        }
							  }
							  
							  InetAddress bcastAddr = InetAddress.getByAddress(addrBytes);
							  return bcastAddr;
	                    }
	                }
	            }
	        } catch (SocketException ex) {
	        	System.out.println(ex.getMessage());
	        }
			return InetAddress.getByName("255.255.255.255"); // ei näin
	    }
		
//		return InetAddress.getByName("130.188.95.255");
//		String localAddr = getLocalAddress().getHostAddress();
//        
//        byte[] addrBytes = InetAddress.getByName(localAddr).getAddress();
//        System.out.println("SyncAgentActivator: local address is "+localAddr);
//        
//        String [] temp = localAddr.split(".");
//        System.out.println("SyncAgentActivator: local address is "+addrBytes[0]+" array idx "+ addrBytes.length);
//        /* 
//         * A-class IPv4 
//         */
//        if (addrBytes[0] < (byte)128)
//        {
//        	byte [] mask = { (byte)255, 0, 0, 0 };
//        	for (int i=0; i < 4; i++) {
//  	          addrBytes[i] |= ((byte)0xFF) ^ mask[i];
//  	        }
//        }
//        /*
//         * B-class IPv4
//         */
//        else if (addrBytes[0] < (byte)192)
//        {
//        	byte [] mask = { (byte)255, (byte)255, 0, 0 };
//        	for (int i=0; i < 4; i++) {
//  	          addrBytes[i] |= ((byte)0xFF) ^ mask[i];
//  	        }
//        }
//        /*
//         * C-class IPv4
//         */
//        else 
//        {
//        	byte [] mask = { (byte)255, (byte)255, (byte)255, 0 };
//        	for (int i=0; i < 4; i++) {
//  	          addrBytes[i] |= ((byte)0xFF) ^ mask[i];
//  	        }
//        }
//        
//        InetAddress bcastAddr = InetAddress.getByAddress(addrBytes);
//        //InetAddress bcastAddr = InetAddress.getByName(localAddr);
//        System.out.println("SyncAgentActivator: broadcast address is "+bcastAddr.getHostAddress());
//        return bcastAddr;

	/**
	 * Publish events to clients. 
	 * @param event
	 */
	public void pushEvents(String event) {
		Socket clientSocket = null;
		PrintWriter out = null;
		String message = "event;"+event;
		if (!checkLocalEvent(event))
		{
			System.out.println("Not forwarding a local event");
			return;
		}
		System.out.println("Pushing to "+clientList.size()+" clients");
		for (int i = 0; i < clientList.size(); i++)
		{
			System.out.println("Pushing to TCP/IP client in: "+ clientList.get(i).getHost() + " port "+ clientList.get(i).getPort());
			try {
				clientSocket = new Socket(clientList.get(i).getHost(),clientList.get(i).getPort());
				out = new PrintWriter(clientSocket.getOutputStream(), true);
				System.out.println("Send message: "+message);
				out.println(message);
				out.close();
				clientSocket.close();
			} catch (UnknownHostException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} 			

		}		
	}
	
	private boolean checkLocalEvent(String event) {
		String[] temp = event.split(",");
		String[] cont;
		System.out.println("Check Local Event: Event has "+temp.length+" elements");
		for (int i = 0; i < temp.length; i++)
		{
			cont = temp[i].split("=");
			if (cont[0].compareTo("remote")==0)
			{
				return false;
			}
		}
		return true;
	}

	/**
	 * Interpret messages from clients. This might have to be removed if run-method blocks the thread. 
	 * @param event
	 */
	public void interpretMessage(String input2) {
		String[] inputs = input2.split(";"); 
		String deviceId = "";
		System.out.println("Trying to interpret received message " + input2);
		if (inputs[0].equalsIgnoreCase("register"))
		{
			try {
				registerClient(new URI(inputs[1]));

				//JRO puukko
				//also publish as an osgi event, to whom it may concern
				Dictionary<String,String> props = new Hashtable<String,String>();
				props.put("URI", inputs[1]);

				if (inputs.length>2) {//deviceId given?
					deviceId = inputs[2];
				}
				
				props.put("deviceId",deviceId);
				
				/* JRO this is stupid; first create an Event, then parse it to string and
				   in SyncAgentServiceImpl, back to Event again. This is because EventAdmin that eats Events is only visible in
				   SyncAgentServiceImpl and the exposed SyncAgentService.publishEvents requires String) 
				 */
				localSyncAgent.publishEvents(eventToString(new Event(REGISTRATION_EVENT, props)));
			}
			catch (URISyntaxException e)
			{
				System.out.println("Malformed client URI: "+ inputs[1]);
			}
		}
		else if(inputs[0].equalsIgnoreCase("deregister")) {
			try {
				deRegisterClient(new URI(inputs[1]));

				//JRO puukko
				//also publish as an osgi event, to whom it may concern
				Dictionary<String,String> props = new Hashtable<String,String>();
				props.put("URI", inputs[1]);

				if (inputs.length>2) {//deviceId given?
					deviceId = inputs[2];
				}
				
				props.put("deviceId", deviceId);
				
				/* JRO this is stupid; first create an Event, then parse it to string and
				   in SyncAgentServiceImpl, back to Event again. This is because EventAdmin is only visible in
				   SyncAgentServiceImpl and the SyncAgentService.publishEvents requires String) 
				 */
				localSyncAgent.publishEvents(eventToString(new Event(DEREGISTRATION_EVENT, props)));
			}
			catch (URISyntaxException e)
			{
				System.out.println("Mallformed client URI: "+ inputs[1]);
			}
		}
		else if(inputs[0].equalsIgnoreCase("event")) {
			
			String event = "";
			for (int i = 1; i < inputs.length; i++)
			{
				if(i<inputs.length-1) {
					event += inputs[i] + ";";
				} else {
					event += inputs[i];
				}
			}
			System.out.println("InterpretMessage got new event: " + event);
			
			localSyncAgent.publishEvents(event);
		}
		
	}

	private String eventToString(Event event) {
		String message;				
		message = "topic="+event.getTopic()+",";
		String[] params = event.getPropertyNames();
		for(int i = 0; i < params.length; i++)
		{
			if (i == params.length-1)
			{
				message += params[i]+"="+event.getProperty(params[i]);
			}
			else 
			{
				message += params[i]+"="+event.getProperty(params[i])+",";  
			}
		}
		return message;
	}
	
	int findClient(URI addr)
	{
		int found = -1;
		for (int i=0; i < clientList.size(); i++)
		{
			if (addr.equals(clientList.get(i)))
				return i;
		}
		return found;
	}

	public int deRegisterClient(URI cliAddr) {
		int index = findClient(cliAddr);
		
		if ( index != -1)
		{
			clientList.remove(index);
		}
		return 0;
	}


	public int registerClient(URI cliAddr) {
		if (findClient(cliAddr) == -1)
		{
			System.out.println("Adding client "+cliAddr.toString());
			clientList.add(cliAddr);
			return 1;
		}
		return 0;
	}


	/**
	 * Check if given client address is exsisting and add it if necessary
	 * @param inetAddress
	 */
	public void checkNewClient(InetAddress inetAddress) {
		try {
			URI newCliURI = new URI("osami://"+inetAddress.getHostAddress()+":"+Activator.SERVER_PORT);
			if (registerClient(newCliURI) == 1)
			{
				System.out.println("Added new client");
			}
		} catch (URISyntaxException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	public void shutdown() {

		//send deregistration message		
		String deviceId = "unknown";
		IDeviceIdentifier di;
        ServiceReference ref = Activator.getBundleContext().getServiceReference(IDeviceIdentifier.class.getName());		
        if (null == ref) {
			System.out.println("DeviceIdentifier service not found");
		} else {
			di = (IDeviceIdentifier)Activator.getBundleContext().getService(ref);
			deviceId = di.GetDeviceId();
		}        
        // Tämä try-catchin jälkeen jos ongelmaa ilmenee
        bcServer.stopThread = true;
        
        try {
			sendBroadCastMessage("deregister;osami://"+getLocalAddress().getHostAddress() + ":" + Activator.SERVER_PORT
					+";"+deviceId);
		} catch (UnknownHostException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
				
		serverThread.shutdown();
	}
}